Java多线程系列--“JUC锁”11之 Semaphore信号量的原理
一、Semaphore简介
Semaphore是一个计数信号量,它的本质是一个"共享锁",是基于AQS实现的,通过state变量来实现共享。通过调用acquire方法,对state值减去一,当调用release的时候,对state值加一。当state变量小于0的时候,在AQS队列中阻塞等待。
信号量维护了一个信号量许可集。线程可以通过调用acquire()来获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以通过release()来释放它所持有的信号量许可。
更多的semaphore介绍见《Java 信号量 Semaphore 入门介绍》,本文从源码层面介绍一下semaphore原理。
二、Semaphore数据结构
Semaphore的UML类图如下:
从图中可以看出:
(01) Semaphore实现的思路跟ReentrantLock非常的相似,包括内部类的结构都是一样的,也是有公平和非公平两种模式。只是不同的是Semaphore是共享锁,支持多个线程同时操作;然而ReentrantLock是互斥锁,同一个时刻只允许一个线程操作。
(02) Sync包括两个子类:"公平信号量"FairSync 和 "非公平信号量"NonfairSync。sync是"FairSync的实例",或者"NonfairSync的实例";默认情况下,sync是NonfairSync(即,默认是非公平信号量)。同样的Sync是一个继承于AQS的抽象类。
示例:
public class SemaphoreDemo { // 创建一个有2个收费口的收费站 private static Semaphore semaphore = new Semaphore(2); public static class RunThread extends Thread { @Override public void run() { // 这里循环100次,模拟车辆非常多,竞争激烈 for (int i = 0; i < 100; i++) { doBusiness(); } } // 这里模拟通过收费口的情况,业务操作 private void doBusiness() { try { // 获取信号 semaphore.acquire(); // 模拟业务操作耗时 Thread.sleep(2000); // 打印信息 System.out.println(Thread.currentThread().getName() + "获取信号"); } catch (Exception e) { e.printStackTrace(); } finally { // 释放信号 semaphore.release(); } } } public static void main(String[] args) throws InterruptedException { // 创建4个线程 RunThread runThread1 = new RunThread(); RunThread runThread2 = new RunThread(); RunThread runThread3 = new RunThread(); RunThread runThread4 = new RunThread(); // 启动线程 runThread1.start(); runThread2.start(); runThread3.start(); runThread4.start(); // 主线程等待runThread1、2、3、4结束之后再往下运行 runThread1.join(); runThread2.join(); runThread3.join(); runThread4.join(); System.out.println("结束"); }
运行程序,你会发现每次打印只会打印2条日志,也就是时候每次最多只会有2辆车同时经过收费站。
三、源码分析
Semaphore源码分析(基于JDK1.8)
在《Java 信号量 Semaphore 入门介绍》的示例里,创建了一个拥有5个许可证的信号量,代码片段如下:
// 初始化信号量,个数为 5 private static Semaphore s = new Semaphore(5);
3.1、非公平信号量
我们看一下构造器:
public Semaphore(int permits) { sync = new NonfairSync(permits); }
从构造器里面可以看出来semaphore默认实现的是非公平锁,我们在看一下NonfairSync类,它是Semaphore的内部类:java.util.concurrent.Semaphore$NonfairSync.java
3.1.1、非公平信号量 类源码
/** * NonFair version */ static final class NonfairSync extends Sync { private static final long serialVersionUID = -2694183684443567898L; NonfairSync(int permits) { super(permits); } protected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires); } }
我们可以看到NonfairSync类继承了Sync,而Sync继承了AQS,从这里其实可以看出来semaphore是基于AQS实现的。
3.2、公平信号量
创建一个拥有5个许可证的信号量,代码片段如下:
// 初始化信号量,个数为 5 private static Semaphore s = new Semaphore(5, true);
构造器:
public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }
3.2.1、公平信号量 源码
static final class FairSync extends Sync { private static final long serialVersionUID = 2014338818796000944L; FairSync(int permits) { super(permits); } protected int tryAcquireShared(int acquires) { for (;;) { if (hasQueuedPredecessors()) return -1; int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } } }
我们可以看到FairSync类继承了Sync,而Sync继承了AQS,从这里其实可以看出来semaphore是基于AQS实现的。
3.2.2、 公平信号量获取
Semaphore中的公平信号量是FairSync。它的获取API如下:
public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } public void acquire(int permits) throws InterruptedException { if (permits < 0) throw new IllegalArgumentException(); sync.acquireSharedInterruptibly(permits); }
Semaphore.acquire方法源码直接是调用FairSync的acquireSharedInterruptibly,也就是进入了AQS的acquireSharedInterruptibly的模板方法里面了,之前我们就讲过了。
acquireSharedInterruptibly()的源码如下:
public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
这个方法定义了一个模板流程:
第一步:是先调用子类的tryAcquireShared方法获取共享锁,也就是获取信号量。
第二步:如果获取信号量成功,即返回值大于等于0,则直接返回。
第三部:如果获取失败,返回值小于0,则调用AQS的doAcquireSharedInterruptibly方法,进入AQS的等待队列里面,等待别人释放资源之后它再去获取。
这里的流程就可以得到一个如下的图:
doAcquireSharedInterruptibly方法的流程我们之前讲解AQS的时候都完全讲解过了,所以只需要分析一下FairSync子类的tryAcquireShared方法的内部源码即可:
protected int tryAcquireShared(int acquires) { for (;;) { // 这里作为公平模式,首先判断一下AQS等待队列里面 // 有没有人在等待获取信号量,如果有人排队了,自己就不去获取了 if (hasQueuedPredecessors()) return -1; // 获取剩余的信号量资源 int available = getState(); // 剩余资源减去我需要的资源,是否小于0 // 如果小于0则说明资源不够了 // 如果大于等于0,说明资源是足够我使用的 int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
上面的源码就是获取信号量的核心流程了:
(1)首先判断一下AQS等待队列里面是否有人在排队,如果是,则自己不尝试获取资源了,乖乖的去排队
(2)如果没有人在排队,获取一下当前剩余的信号量available,然后减去自己需要的信号量acquires,得到减去后的结果remaining。
(3)如果remaining小于0,直接返回remaining,说明资源不够,获取失败了,这个时候就会进入AQS等待队列等待。
(4)如果remaining 大于等于0,则执行CAS操作compareAndSetState竞争资源,如果成功了,说明自己获取信号量成功,如果失败了同样进入AQS等待队列。
这里画一下公平模式FairSync的tryAcquireShared流程图,以及整个公平模式的acquire方法的流程图:
上面就是我分析得到的FairSync公平模式的acquire获取信号量的全部流程图了。
AQS的hasQueuedPredecessors():
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
说明:tryAcquireShared()的作用是尝试获取acquires个信号量许可数。
对于Semaphore而言,state表示的是“当前可获得的信号量许可数”。
3.2.3、 公平信号量的释放
Semaphore中公平信号量(FairSync)的释放API如下:
public void release() { sync.releaseShared(1); } public void release(int permits) { if (permits < 0) throw new IllegalArgumentException(); sync.releaseShared(permits); }
我们继续来分析releaseShared方法,进入到AQS的releaseShard释放资源的模板方法:
public final boolean releaseShared(int arg) { // 1. 调用子类的tryReleaseShared释放资源 if (tryReleaseShared(arg)) { // 释放资源成功,调用doReleaseShared唤醒等待队列中等待资源的线程 doReleaseShared(); return true; } return false; }
其它和非公平信号量的释放相同。
这里的模板流程有:
(1)调用子类的tryReleaseShared去释放资源,即释放信号量
(2)如果释放成功了,则调用doReleaseShared唤醒AQS中等待资源的线程,将资源传播下去,如果释放失败,即返回小于等于0,则直接返回。
所以,这里除了AQS的核心模板流程之外,具体释放逻辑就是Sync的tryReleaseShared方法的源码了,我们继续来查看:
Sync的tryReleaseShard源码:
protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); // 这里就是将释放的信号量资源加回去而已 int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); // 尝试CAS设置资源,成功直接返回,失败则进入下一循环重试 if (compareAndSetState(current, next)) return true; } }
这里的逻辑非常简单了,无非是不断尝试CAS将资源加回去而已。
针对Semaphore释放资源的流程,我这边也画了一副图出来:
非公平模式NonfairSync跟公平模式唯一的区别就是在tryAcquireShared上的实现不一样,其它的完全都是一致的,我们下面就看一下NonfairSync的tryAcquireShared方法源码:
NonfairSync的tryAcquireShared方法源码:
protected int tryAcquireShared(int acquires) { return nonfairTryAcquireShared(acquires); }
这里是直接调用了Sync的nonfairTryAcquireShared方法源码,我们继续往下看:
final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); // 上面公平模式需要看下等待队列是否有人 // 这里是直接去尝试获取资源啊,根本不管是否有人 int remaining = available - acquires; if (remaining < 0 || // 如果remaining剩余资源 >= 0 则执行CAS操作 compareAndSetState(available, remaining)) return remaining; } }
这里非公平锁的源码流程就非常简单了:
(1)对比上面的公平模式,需要判断AQS等待队列是否有人在等待。而这里非公平模式不管有没有人在等
(2)如果剩余可用资源remaining >= 0,则直接CAS去争抢资源,成功则返回,失败则重试。
四、总结
1、"公平信号量"和"非公平信号量"的区别
"公平信号量"和"非公平信号量"的释放信号量的机制是一样的!不同的是它们获取信号量的机制:线程在尝试获取信号量许可时,对于公平信号量而言,如果当前线程不在CLH队列的头部,则排队等候;而对于非公平信号量而言,无论当前线程是不是在CLH队列的头部,它都会直接获取信号量。该差异具体的体现在,它们的tryAcquireShared()函数的实现不同。
2、一般而言,非公平时候的吞吐量要高于公平锁”,这是为什么呢?
非公平锁性能高于公平锁性能的原因:在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。假设线程A持有一个锁,并且线程B请求这个锁。由于锁被A持有,因此B将被挂起。当A释放锁时,B将被唤醒,因此B会再次尝试获取这个锁。与此同时,如果线程C也请求这个锁,那么C很可能会在B被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面:B获得锁的时刻并没有推迟,C更早的获得了锁,并且吞吐量也提高了。当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。在这些情况下,插队带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)可能不会出现。
3、Semaphore与jdk中的Lock
的区别
1. 使用Lock.unlock()之前,该线程必须事先持有这个锁(通过Lock.lock()获取),如下:
public class LockTest { public static void main(String[] args) { Lock lock=new ReentrantLock(); lock.unlock(); } }
则会抛出异常,因为该线程事先并没有获取lock对象的锁:
Exception in thread "main" java.lang.IllegalMonitorStateException at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:155) at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1260) at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:460) at LockTest.main(LockTest.java:12)
对于Semaphore来讲,如下:
public class SemaphoreTest { public static void main(String[] args) { Semaphore semaphore=new Semaphore(1);//总共有1个许可 System.out.println("可用的许可数目为:"+semaphore.availablePermits()); semaphore.release(); System.out.println("可用的许可数目为:"+semaphore.availablePermits()); } }
结果如下:
可用的许可数目为:1
可用的许可数目为:2
i. 并没有抛出异常,也就是线程在调用release()之前并不要求先调用acquire()
ii. 我们看到可用的许可数目增加了一个,但我们的初衷是保证只有一个许可来达到互斥排他锁的目的,所以这里要注意一下
参考:https://mp.weixin.qq.com/s/-wlEUriTNRSN7V-UxsFm6g